iT邦幫忙

2025 iThome 鐵人賽

DAY 20
1
AI & Data

讓電腦聽懂人話:30 天 NLP 入門系列 第 20

Day 20|模擬大腦的世界(下):Backpropagation 與 FNN 實作

  • 分享至 

  • xImage
  •  

引言

昨天我們認識了神經網路的基本概念包括:神經元如何接收輸入、加權求和,透過激活函數如何轉換成非線性模式並產生輸出。

一個神經元的計算會需要有輸入特徵 𝑥,然後乘上權重 𝑤(weight)加上偏置 𝑏(bias)。但這些數值是可以透過模型的學習去自動調整的哦~

所以今天我們要更進一步看看讓神經網路可以自動學習背後的演算法 反向傳播(Backpropagation),還有一起實作一個簡單的 Feedforward Neural Network!!

反向傳播(Backpropagation)

我們訓練模型是為了要讓模型有好的表現!但 「好表現」具體來說是什麼意思?
模型主要的任務是要做預測,那我們就是希望模型「預測的結果」和「真實結果」的差距越小越好。所以有一個概念叫做 「損失」(Loss) 指得就是模型預測和真實標籤之間的差距。而我們的目標就是要 「最小化損失」

整個流程可以分為以下五個步驟:

  1. 輸入資料 → 前向傳播 → 預測值
  2. 計算損失
  3. 損失 → 反向傳播 → 計算梯度
  4. 用梯度更新權重和偏置
  5. 重複多次(epoch)直到損失收斂


圖片來源:https://medium.com/analytics-vidhya/backpropagation-for-dummies-e069410fa585

那我們來介紹以下四個核心概念:

1. 前向傳播(Forward Pass)

前向傳播就是用以上這個公式先計算出一個數值 𝑦

2. 計算損失

用損失函數(Loss Function)計算出預測值 𝑦 跟真實值的差距。
損失函數有很多種,在分類任務中常用的是 交叉熵(Cross-Entropy Loss)

3. 反向傳播(Backward Pass)

利用 鏈式法則(Chain Rule) 計算每個權重對損失的影響,再將誤差從輸出層往後傳回到輸入層。

4. 梯度下降(Gradient Descent)

利用損失函數的「斜率」來決定怎麼調整權重。也會透過設定學習率 𝜂 來控制每次更新的步伐。可以想像我們在一座山坡上(損失函數),要往山谷(最佳解)走。梯度就像坡度,學習率(Learning Rate)就像走路的步伐大小。

程式實作

這段程式實作一樣會使用 kaggle 的 IMDb 50K Cleaned Movie Reviews 資料集。然後會用 Word2Vec 轉成 embedding 之後,再用 PyTorch 建置一個簡單的前饋神經網路。
https://ithelp.ithome.com.tw/upload/images/20251002/20178719hwbUXwdbID.png

1. 讀取資料

import kagglehub
import os
import pandas as pd

path = kagglehub.dataset_download("ibrahimqasimi/imdb-50k-cleaned-movie-reviews")
csv_path = os.path.join(path, "IMDB_cleaned.csv")
df = pd.read_csv(csv_path)

2. 資料前處理

  • 訓練 Word2Vec 模型,將特徵用向量表示
  • 資料需要轉成 tensor 張量 的形式
import torch
from gensim.models import Word2Vec

# 將句子拆成單詞轉成 embedding
sentences = [review.split() for review in df['cleaned_review']] 
w2v_model = Word2Vec(sentences, vector_size=50, window=5, min_count=1, workers=4)

# 將每篇文章的單詞向量平均
def embed_sentence(sentence):
    vecs = [w2v_model.wv[word] for word in sentence if word in w2v_model.wv]
    if len(vecs) == 0:
        return torch.zeros(50)
    return torch.tensor(vecs).mean(dim=0)

# feature
X = torch.stack([embed_sentence(s.split()) for s in df['cleaned_review']])
# label
y = torch.tensor([1 if s=='positive' else 0 for s in df['sentiment']], dtype=torch.float32)

3. 切分資料集&將資料裝成 Data Loader

使用 DataLoader 可以:

  • 自動分批 (batch) 訓練,節省記憶體
  • 打亂 (shuffle) 訓練資料,避免模型只學到資料順序的模式
  • 搭配 GPU 使用
from torch.utils.data import DataLoader, TensorDataset

# training set:test set => 8:2
train_size = int(0.8 * len(X))
X_train, X_test = X[:train_size], X[train_size:]
y_train, y_test = y[:train_size], y[train_size:]

# 建立 Tensor Dataset
train_ds = TensorDataset(X_train, y_train)
test_ds = TensorDataset(X_test, y_test)
# 建立 Data Loader
train_loader = DataLoader(train_ds, batch_size=64, shuffle=True)
test_loader = DataLoader(test_ds, batch_size=64)

4. 建立模型

  • 定義維度:
    • input_dim:資料轉成向量是 50 維
    • hidden_dim:自訂隱藏層維度 32 維
    • output_dim:二元分類所以是 1 個值
  • 定義內層:
    • fc1:輸入層 → 隱藏層,維度 [50 → 32]。
    • relu:隱藏層激活函數。
    • fc2:隱藏層 → 輸出層,輸出 1 個值。
    • sigmoid:輸出層激活函數,將結果壓縮到 [0, 1],適合二元分類。
  • 定義前向傳播 forward():把輸入 x 依序經過 隱藏層 → 激活函數 → 輸出層 → Sigmoid
import torch.nn as nn
import torch.optim as optim

class SimpleMLP(nn.Module):
    def __init__(self, input_dim=50, hidden_dim=32, output_dim=1):
        super().__init__()
        self.fc1 = nn.Linear(input_dim, hidden_dim)
        self.relu = nn.ReLU()
        self.fc2 = nn.Linear(hidden_dim, output_dim)
        self.sigmoid = nn.Sigmoid()
    
    def forward(self, x):
        x = self.relu(self.fc1(x))
        x = self.sigmoid(self.fc2(x))
        return x.squeeze()

model = SimpleMLP()
criterion = nn.BCELoss() # 損失函數
optimizer = optim.Adam(model.parameters(), lr=0.001) # 優化器、學習率

5. 訓練模型

  • 設定訓練輪數 (epochs) 為 5 輪
  • 使用訓練模式 model.train()
  • 印出每輪的損失(可以看到有逐漸下降)
epochs = 5
for epoch in range(epochs):
    model.train() 
    total_loss = 0
    for xb, yb in train_loader:  # 批次訓練
        optimizer.zero_grad()  # 每次訓練梯度歸零
        preds = model(xb)  # 前向傳播
        loss = criterion(preds, yb)  # 計算損失
        loss.backward()  # 反向傳播
        optimizer.step()  # 更新權重
        total_loss += loss.item()
    print(f"Epoch {epoch+1}/{epochs}, Loss: {total_loss/len(train_loader):.4f}")
# === Output ===
Epoch 1/5, Loss: 0.6847
Epoch 2/5, Loss: 0.6729
Epoch 3/5, Loss: 0.6620
Epoch 4/5, Loss: 0.6553
Epoch 5/5, Loss: 0.6460

6. 評估模型

  • 使用評估模式 model.eval()
from sklearn.metrics import precision_score, recall_score, f1_score

y_true = []
y_pred = []

model.eval()
with torch.no_grad():  # 關閉梯度計算
    for xb, yb in test_loader:
        outputs = model(xb)
        preds = (outputs >= 0.5).float()
        y_true.extend(yb.cpu().numpy())
        y_pred.extend(preds.cpu().numpy())

# 計算 metrics
acc = sum([1 if p == t else 0 for p, t in zip(y_pred, y_true)]) / len(y_true)
precision = precision_score(y_true, y_pred)
recall = recall_score(y_true, y_pred)
f1 = f1_score(y_true, y_pred)

print(f"Accuracy : {acc:.4f}")
print(f"Precision: {precision:.4f}")
print(f"Recall   : {recall:.4f}")
print(f"F1-score : {f1:.4f}")
# === Output ===
Accuracy : 0.6600
Precision: 0.7208
Recall   : 0.5111
F1-score : 0.5981

結語

今天我們更深入的了解了模型到底是怎麼做到「自己學習」這件事,是透過 前向傳播 → 計算損失 → 反向傳播 → 梯度下降 的步驟去調整權重,慢慢讓損失下降~

最後也實作了一個簡單的前饋神經網路。雖然這個模型已經可以對每篇文章做分類,但它其實有一個缺點!就是它完全忽略文字的 順序性。它看到的只是「一袋詞」的平均特徵,而無法理解文字的脈絡或語句的前後關係。

所以接下來我們會進入 「序列模型」 的世界,從 RNN 開始,學會如何讓模型「記住長長的故事」,再進一步用 LSTM 處理長短期依賴問題,讓模型更能具備像人一樣的 「記憶力」!!

References


上一篇
Day 19|模擬大腦的世界(上):Neural Network 入門
下一篇
Day 21|模型的記憶力:RNN
系列文
讓電腦聽懂人話:30 天 NLP 入門22
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言